//NOTE: Put comments for the headers included
#include <windows.h>
#include <winuser.h>			// Windows constants
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <typeinfo>
#include <string>

#define _USE_MATH_DEFINES
#include <math.h>

#include "GL/glut.h"
#include <GL/gl.h>
#include <GL/glu.h>

#include "IL/ilut.h"
#include "IL/il.h"
#include "IL/ilu.h"

#include "Game.h"

using namespace std;

/* ************************************************************************ */
#define WIN32_LEAN_AND_MEAN

#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")
#pragma comment(linker, "/subsystem:windows")

#define WND_CLASS_NAME  "OpenGL Window Class"

/*************************** Constants and Macros ***************************/
int			SCREEN_WIDTH			= 800;
int			SCREEN_HEIGHT			= 600;
const int   SCREEN_BPP				= 32;
const bool  USE_FULLSCREEN			= false;
const char  *APP_TITLE				= "Apsis";

const int INPUT_FRAMES_PER_SEC		= 30;
const int INPUT_FRAME_DELAY			= CLOCKS_PER_SEC / INPUT_FRAMES_PER_SEC;
const int DISPLAY_FRAMES_PER_SEC	= 40;
const int DISPLAY_FRAME_DELAY		= CLOCKS_PER_SEC / DISPLAY_FRAMES_PER_SEC;
const int PHYSICS_FRAMES_PER_SEC	= 60;
const int PHYSICS_FRAME_DELAY		= CLOCKS_PER_SEC / PHYSICS_FRAMES_PER_SEC;


/********************************* Globals **********************************/
HDC       g_hdc;                  // global device context
HGLRC     g_hrc;                  // global rendering context
BOOL      g_isFullscreen = TRUE;  // toggles fullscreen and windowed play
BOOL      g_isActive = TRUE;      // false if window is minimized
HWND      g_hwnd = NULL;          // handle of our window
HINSTANCE g_hInstance;            // application instance


/******************************** Prototypes ********************************/
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

BOOL    SetupWindow(const char *title, int width, int height, int bits, bool isFullscreen);
BOOL    KillWindow();

GLvoid  ResizeScene(GLsizei width, GLsizei height);
BOOL    InitializeScene();
BOOL    DisplayScene();
BOOL    Cleanup();


/*****************************************************************************
WinMain()

Windows entry point
*****************************************************************************/
int WINAPI WinMain(HINSTANCE g_hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	g_error = glGetError();

	MSG   msg;       // message
	BOOL  isDone;    // flag indicating when the app is done

	// if the window is set up correctly, we can proceed with the message loop, else we exit
	if (SetupWindow(APP_TITLE, SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, USE_FULLSCREEN))
		isDone = FALSE;
	else
		isDone = TRUE;

	InitializeDevIL();
	InitializeGame();
	DisplayScene();
	SwapBuffers(g_hdc);

	//NOTE: Need to set timers for this.
	// Main Loop
	while (!isDone)
	{
		// Message / Event Handling
		if(PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE))
		{
			if (msg.message == WM_QUIT)  
			{
				// Quitting application
				isDone = TRUE;
			}
			else
			{
				// translate and dispatch to event queue
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
		}

		// If the window is active / not minimized
		if (g_isActive)
		{
			// Updating the clock
			clock_ticks = clock();

			if (physics_timer.Lapse() >= PHYSICS_FRAME_DELAY) {	physics_timer.Reset();
				UpdateGame();
			}
			
			if (display_timer.Lapse() >= DISPLAY_FRAME_DELAY) { display_timer.Reset();
				// update the Display
				DisplayScene();
				SwapBuffers(g_hdc);
			}

			if (input_timer.Lapse() >= INPUT_FRAME_DELAY) { input_timer.Reset(); // Input timer reset has to be placed after the physics timer reset
				Mouse.SetPosition(Mouse.position.x,	Mouse.position.y);
				Mouse.scroll_delta = 0;
			}
		}
	}

	Cleanup();
	KillWindow();

	return msg.wParam;
} // end WinMain()


/*****************************************************************************
WndProc()

Windows message handler
*****************************************************************************/
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//cout << "Message: " << message << " wParam: " << toupper(wParam) << endl; 
	switch(message)
	{
	/* Keyboard Input */
	case WM_KEYDOWN: // Keyboard Key Press
		{
			Keyboard.AddKey(wParam);
		} break;
	
	case WM_KEYUP: // Keyboard Key Release
		{
			//MessageBox(NULL, wParam, "Debug", MB_OK | MB_ICONEXCLAMATION);
			Keyboard.RemoveKey(wParam);
		} break;

	/* Mouse Input */
	case WM_MOUSEMOVE:
		{
			Mouse.SetPosition(LOWORD(lParam), SCREEN_HEIGHT - HIWORD(lParam));
		} break;
	case WM_MOUSEWHEEL:
		{
			Mouse.scroll_delta = GET_WHEEL_DELTA_WPARAM(wParam)/120;
		} break;
	case WM_LBUTTONDOWN:
		{
			Mouse.isLeftDown = true;
		} break;
	case WM_LBUTTONUP:
		{
			Mouse.isLeftDown = false;
		} break;
	case WM_RBUTTONDOWN:
		{
			Mouse.isRightDown = true;
		} break;
	case WM_RBUTTONUP:
		{
			Mouse.isRightDown = false;
		} break;
	case WM_MBUTTONDOWN:
		{
			Mouse.isMiddleDown = true;
		} break;
	case WM_MBUTTONUP:
		{
			Mouse.isMiddleDown = false;
		} break;

	case WM_ACTIVATE:  // watch for the window being minimized and restored
		{
			if (!HIWORD(wParam))
			{
				// program was restored or maximized
				g_isActive = TRUE;
			}
			else
			{
				// program was minimized
				g_isActive=FALSE;
			}
			return 0;
		}

	case WM_SYSCOMMAND:  // look for screensavers and powersave mode
		{
			switch (wParam)
			{
			case SC_SCREENSAVE:     // screensaver trying to start
			case SC_MONITORPOWER:   // monitor going to powersave mode
				// returning 0 prevents either from happening
				return 0;
			default:
				break;
			}
		} break;

	case WM_CLOSE:    // window is being closed
		{
			// send WM_QUIT to message queue
			PostQuitMessage(0);

			return 0;
		}

	case WM_SIZE:
		{
			// update perspective with new width and height
			ResizeScene(LOWORD(lParam), HIWORD(lParam));
			return 0;
		}

	case WM_CHAR:
		{
			switch (toupper(wParam))
			{
			case VK_ESCAPE:
				{
					// send WM_QUIT to message queue
					PostQuitMessage(0);
					return 0;
				}
			default:
				break;
			};
		} break;

	default:
		break;
	}

	return (DefWindowProc(hwnd, message, wParam, lParam));
} // end WndProc()


/*****************************************************************************
DisplayScene()

The work of the application is done here. This is called every frame, and
handles the actual rendering of the scene.
*****************************************************************************/
BOOL DisplayScene()
{
	glClearColor(0.0, 0.0, 0.0, 0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	
	DrawGame();

	return TRUE;
} // end DisplayScene()


/*****************************************************************************
SetupWindow()

Create the window and everything else we need, including the device and
rendering context. If a fullscreen window has been requested but can't be
created, the user will be prompted to attempt windowed mode. Finally,
InitializeScene is called for application-specific setup.

Returns TRUE if everything goes well, or FALSE if an unrecoverable error
occurs. Note that if this is called twice within a program, KillWindow needs
to be called before subsequent calls to SetupWindow.
*****************************************************************************/
BOOL SetupWindow(const char *title, int width, int height, int bits, bool isFullscreen)
{
	// set the global flag
	g_isFullscreen = isFullscreen;

	// get our instance handle
	g_hInstance = GetModuleHandle(NULL);

	WNDCLASSEX  wc;    // window class

	// fill out the window class structure
	wc.cbSize         = sizeof(WNDCLASSEX);
	wc.style          = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc    = WndProc;
	wc.cbClsExtra     = 0;
	wc.cbWndExtra     = 0;
	wc.hInstance      = g_hInstance;
	wc.hIcon          = LoadIcon(NULL, IDI_APPLICATION);  // default icon
	wc.hIconSm        = LoadIcon(NULL, IDI_WINLOGO);      // windows logo small icon
	wc.hCursor        = LoadCursor(NULL, IDC_ARROW);      // default arrow
	wc.hbrBackground  = NULL;							  // no background needed
	wc.lpszMenuName   = NULL;							  // no menu
	wc.lpszClassName  = WND_CLASS_NAME;

	// register the windows class
	if (!RegisterClassEx(&wc))
	{
		MessageBox(NULL,"Unable to register the window class", "Error", MB_OK | MB_ICONEXCLAMATION);

		// exit and return FALSE
		return FALSE;
	}

	// if we're in fullscreen mode, set the display up for it
	if (g_isFullscreen)
	{
		// set up the device mode structure
		DEVMODE screenSettings;
		memset(&screenSettings,0,sizeof(screenSettings));

		screenSettings.dmSize       = sizeof(screenSettings);
		screenSettings.dmPelsWidth  = width;    // screen width
		screenSettings.dmPelsHeight = height;   // screen height
		screenSettings.dmBitsPerPel = bits;     // bits per pixel
		screenSettings.dmFields     = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;

		// attempt to switch to the resolution and bit depth we've selected
		if (ChangeDisplaySettings(&screenSettings, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
		{
			// if we can't get fullscreen, let them choose to quit or try windowed mode
			if (MessageBox(NULL, "Cannot run in the fullscreen mode at the selected resolution\n"
				"on your video card. Try windowed mode instead?",
				"Error",
				MB_YESNO | MB_ICONEXCLAMATION) == IDYES)
			{
				g_isFullscreen = FALSE;
			}
			else
			{
				return FALSE;
			}
		}
	}

	DWORD dwExStyle;
	DWORD dwStyle;

	// set the window style appropriately, depending on whether we're in fullscreen mode
	if (g_isFullscreen)
	{
		dwExStyle = WS_EX_APPWINDOW;
		dwStyle = WS_POPUP;           // simple window with no borders or title bar
		ShowCursor(FALSE);            // hide the cursor for now
	}
	else
	{
		dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
		dwStyle = WS_OVERLAPPEDWINDOW;
	}

	// set up the window we're rendering to so that the top left corner is at (0,0)
	// and the bottom right corner is (height,width)
	RECT  windowRect;
	windowRect.left = 0;
	windowRect.right = (LONG) width;
	windowRect.top = 0;
	windowRect.bottom = (LONG) height;

	// change the size of the rect to account for borders, etc. set by the style
	AdjustWindowRectEx(&windowRect, dwStyle, FALSE, dwExStyle);

	// class registered, so now create our window
	g_hwnd = CreateWindowEx(dwExStyle,          // extended style
		WND_CLASS_NAME,     // class name
		title,              // app name
		dwStyle |           // window style
		WS_CLIPCHILDREN |   // required for
		WS_CLIPSIBLINGS,    // using OpenGL
		0, 0,               // x,y coordinate
		windowRect.right - windowRect.left, // width
		windowRect.bottom - windowRect.top, // height
		NULL,               // handle to parent
		NULL,               // handle to menu
		g_hInstance,        // application instance
		NULL);              // no extra params

	// see if our window handle is valid
	if (!g_hwnd)
	{
		MessageBox(NULL, "Unable to create window", "Error", MB_OK | MB_ICONEXCLAMATION);
		return FALSE;
	}

	// get a device context
	if (!(g_hdc = GetDC(g_hwnd)))
	{
		MessageBox(NULL,"Unable to create device context", "Error", MB_OK | MB_ICONEXCLAMATION);
		return FALSE;
	}

	// set the pixel format we want
	PIXELFORMATDESCRIPTOR pfd = {
		sizeof(PIXELFORMATDESCRIPTOR),  // size of structure
		1,                              // default version
		PFD_DRAW_TO_WINDOW |            // window drawing support
		PFD_SUPPORT_OPENGL |            // OpenGL support
		PFD_DOUBLEBUFFER,               // double buffering support
		PFD_TYPE_RGBA,                  // RGBA color mode
		bits,                           // 32 bit color mode
		0, 0, 0, 0, 0, 0,               // ignore color bits, non-palettized mode
		0,                              // no alpha buffer
		0,                              // ignore shift bit
		0,                              // no accumulation buffer
		0, 0, 0, 0,                     // ignore accumulation bits
		16,                             // 16 bit z-buffer size
		8,                              // no stencil buffer
		0,                              // no auxiliary buffer
		PFD_MAIN_PLANE,                 // main drawing plane
		0,                              // reserved
		0, 0, 0 };                      // layer masks ignored

		GLuint  pixelFormat;

		// choose best matching pixel format
		if (!(pixelFormat = ChoosePixelFormat(g_hdc, &pfd)))
		{
			MessageBox(NULL, "Can't find an appropriate pixel format", "Error", MB_OK | MB_ICONEXCLAMATION);
			return FALSE;
		}

		// set pixel format to device context
		if(!SetPixelFormat(g_hdc, pixelFormat,&pfd))
		{
			MessageBox(NULL, "Unable to set pixel format", "Error", MB_OK | MB_ICONEXCLAMATION);
			return FALSE;
		}

		// create the OpenGL rendering context
		if (!(g_hrc = wglCreateContext(g_hdc)))
		{
			MessageBox(NULL, "Unable to create OpenGL rendering context", "Error",MB_OK | MB_ICONEXCLAMATION);
			return FALSE;
		}

		// now make the rendering context the active one
		if(!wglMakeCurrent(g_hdc, g_hrc))
		{
			MessageBox(NULL,"Unable to activate OpenGL rendering context", "ERROR", MB_OK | MB_ICONEXCLAMATION);
			return FALSE;
		}

		// show the window in the forground, and set the keyboard focus to it
		ShowWindow(g_hwnd, SW_SHOW);
		SetForegroundWindow(g_hwnd);
		SetFocus(g_hwnd);

		// set up the perspective for the current screen size
		ResizeScene(width, height);

		// do one-time initialization
		if (!InitializeScene())
		{
			MessageBox(NULL, "Initialization failed", "Error", MB_OK | MB_ICONEXCLAMATION);
			return FALSE;
		}

		return TRUE;
} // end SetupWindow()


/*****************************************************************************
KillWindow()

Deletes the DC, RC, and Window, and restores the original display.
*****************************************************************************/
BOOL KillWindow()
{
	// restore the original display if we're in fullscreen mode
	if (g_isFullscreen)
	{
		ChangeDisplaySettings(NULL, 0);
		ShowCursor(TRUE);
	}

	// if we have an RC, release it
	if (g_hrc)
	{
		// release the RC
		if (!wglMakeCurrent(NULL,NULL))
		{
			MessageBox(NULL, "Unable to release rendering context", "Error", MB_OK | MB_ICONINFORMATION);
		}

		// delete the RC
		if (!wglDeleteContext(g_hrc))
		{
			MessageBox(NULL, "Unable to delete rendering context", "Error", MB_OK | MB_ICONINFORMATION);
		}

		g_hrc = NULL;
	}

	// release the DC if we have one
	if (g_hdc && !ReleaseDC(g_hwnd, g_hdc))
	{
		MessageBox(NULL, "Unable to release device context", "Error", MB_OK | MB_ICONINFORMATION);
		g_hdc = NULL;
	}

	// destroy the window if we have a valid handle
	if (g_hwnd && !DestroyWindow(g_hwnd))
	{
		MessageBox(NULL, "Unable to destroy window", "Error", MB_OK | MB_ICONINFORMATION);
		g_hwnd = NULL;
	}

	// unregister our class so we can create a new one if we need to
	if (!UnregisterClass(WND_CLASS_NAME, g_hInstance))
	{
		MessageBox(NULL, "Unable to unregister window class", "Error", MB_OK | MB_ICONINFORMATION);
		g_hInstance = NULL;
	}

	return TRUE;
} // end KillWindow()


/*****************************************************************************
ResizeScene()

Called once when the application starts and again every time the window is
resized by the user.
*****************************************************************************/
GLvoid ResizeScene(GLsizei width, GLsizei height)
{
	// avoid divide by zero
	if (height==0)
		height=1;

	SCREEN_HEIGHT = height;
	SCREEN_WIDTH = width;

	// reset the viewport to the new dimensions
	glViewport(0, 0, width, height);

	// set up the projection, without toggling the projection mode
	float aspect_ratio = (float)SCREEN_WIDTH / (float)SCREEN_HEIGHT;
	float field_of_view = (height >= width) ?
		current_camera->target_fov :
		atan(tan(current_camera->target_fov * M_PI / 360) / aspect_ratio) * 360 / M_PI;

	// select the projection matrix and clear it out
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	// set the perspective with the appropriate aspect ratio
	gluPerspective(	field_of_view, 
					aspect_ratio,
					current_camera->near_clipping_distance,
					current_camera->far_clipping_distance);

	// select modelview matrix and clear it out
	glMatrixMode(GL_MODELVIEW);
} // end ResizeScene()


/*****************************************************************************
InitializeScene()

Performs one-time application-specific setup. Returns FALSE on any failure.
*****************************************************************************/
BOOL InitializeScene()
{
	//glEnable(GL_LIGHTING);
	//glEnable(GL_LIGHT0);

	//glShadeModel(GL_SMOOTH);
	glEnable(GL_DEPTH_TEST);

	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);

	glEnable(GL_TEXTURE_2D);

	return TRUE;
} // end InitializeScene()


/*****************************************************************************
Cleanup()

Called at the end of successful program execution.
*****************************************************************************/
BOOL Cleanup()
{
	return TRUE;
} // end Cleanup()